// Copyright (C) 2010, 2011, 2012 GlavSoft LLC.
// All rights reserved.
//
//-------------------------------------------------------------------------
// This file is part of the TightVNC software.  Please visit our Web site:
//
//                       http://www.tightvnc.com/
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//-------------------------------------------------------------------------
//
package com.glavsoft.rfb.protocol;

import com.glavsoft.core.SettingsChangedEvent;
import com.glavsoft.exceptions.*;
import com.glavsoft.rfb.*;
import com.glavsoft.rfb.client.ClientToServerMessage;
import com.glavsoft.rfb.client.FramebufferUpdateRequestMessage;
import com.glavsoft.rfb.client.SetEncodingsMessage;
import com.glavsoft.rfb.client.SetPixelFormatMessage;
import com.glavsoft.rfb.encoding.PixelFormat;
import com.glavsoft.rfb.encoding.decoder.DecodersContainer;
import com.glavsoft.rfb.protocol.state.HandshakeState;
import com.glavsoft.rfb.protocol.state.ProtocolState;
import com.glavsoft.transport.Reader;
import com.glavsoft.transport.Writer;

import java.util.logging.Logger;

public class Protocol implements ProtocolContext, IChangeSettingsListener {

    private ProtocolState state;
    private final Logger logger = Logger.getLogger("com.glavsoft.rfb.protocol");
    private final IPasswordRetriever passwordRetriever;
    private final ProtocolSettings settings;
    private int fbWidth;
    private int fbHeight;
    private PixelFormat pixelFormat;
    private final Reader reader;
    private final Writer writer;
    private String remoteDesctopName;
    private MessageQueue messageQueue;
    private final DecodersContainer decoders;
    private SenderTask senderTask;
    private ReceiverTask receiverTask;
    private IRfbSessionListener rfbSessionListener;
    private IRepaintController repaintController;
    private PixelFormat serverPixelFormat;
    private Thread senderThread;
    private Thread receiverThread;

    public Protocol(Reader reader, Writer writer,
            IPasswordRetriever passwordRetriever, ProtocolSettings settings) {
        this.reader = reader;
        this.writer = writer;
        this.passwordRetriever = passwordRetriever;
        this.settings = settings;
        decoders = new DecodersContainer();
        decoders.instantiateDecodersWhenNeeded(settings.encodings);
        state = new HandshakeState(this);
    }

    @Override
    public void changeStateTo(ProtocolState state) {
        this.state = state;
    }

    public void handshake() throws UnsupportedProtocolVersionException, UnsupportedSecurityTypeException,
            AuthenticationFailedException, TransportException, FatalException {
        while (state.next()) {
            continue;
        }
        this.messageQueue = new MessageQueue();
    }

    @Override
    public PixelFormat getPixelFormat() {
        return pixelFormat;
    }

    @Override
    public void setPixelFormat(PixelFormat pixelFormat) {
        this.pixelFormat = pixelFormat;
        if (repaintController != null) {
            repaintController.setPixelFormat(pixelFormat);
        }
    }

    @Override
    public String getRemoteDesktopName() {
        return remoteDesctopName;
    }

    @Override
    public void setRemoteDesktopName(String name) {
        remoteDesctopName = name;
    }

    @Override
    public int getFbWidth() {
        return fbWidth;
    }

    @Override
    public void setFbWidth(int fbWidth) {
        this.fbWidth = fbWidth;
    }

    @Override
    public int getFbHeight() {
        return fbHeight;
    }

    @Override
    public void setFbHeight(int fbHeight) {
        this.fbHeight = fbHeight;
    }

    @Override
    public IPasswordRetriever getPasswordRetriever() {
        return passwordRetriever;
    }

    @Override
    public ProtocolSettings getSettings() {
        return settings;
    }

    @Override
    public Logger getLogger() {
        return logger;
    }

    @Override
    public Writer getWriter() {
        return writer;
    }

    @Override
    public Reader getReader() {
        return reader;
    }

    /**
     * Following the server initialisation message it's up to the client to send
     * whichever protocol messages it wants. Typically it will send a
     * SetPixelFormat message and a SetEncodings message, followed by a
     * FramebufferUpdateRequest. From then on the server will send
     * FramebufferUpdate messages in response to the client's
     * FramebufferUpdateRequest messages. The client should send
     * FramebufferUpdateRequest messages with incremental set to true when it
     * has finished processing one FramebufferUpdate and is ready to process
     * another. With a fast client, the rate at which FramebufferUpdateRequests
     * are sent should be regulated to avoid hogging the network.
     */
    public void startNormalHandling(IRfbSessionListener rfbSessionListener,
            IRepaintController repaintController, ClipboardController clipboardController) {
        this.rfbSessionListener = rfbSessionListener;
        this.repaintController = repaintController;
//		if (settings.getBitsPerPixel() == 0) {
//			settings.setBitsPerPixel(pixelFormat.bitsPerPixel); // the same the server sent when not initialized yet
//		}
        serverPixelFormat = pixelFormat;
        serverPixelFormat.trueColourFlag = 1; // correct flag - we don't support color maps
        setPixelFormat(createPixelFormat(settings));
        sendMessage(new SetPixelFormatMessage(pixelFormat));
        logger.fine("sent: " + pixelFormat);

        sendSupportedEncodingsMessage(settings);
        settings.addListener(this); // to support pixel format (color depth), and encodings changes
        settings.addListener(repaintController);

        sendRefreshMessage();
        senderTask = new SenderTask(messageQueue, writer, this);
        senderThread = new Thread(senderTask);
        senderThread.start();
        decoders.resetDecoders();
        receiverTask = new ReceiverTask(
                reader, repaintController,
                clipboardController,
                decoders, this);
        receiverThread = new Thread(receiverTask);
        receiverThread.start();
    }

    @Override
    public void sendMessage(ClientToServerMessage message) {
        messageQueue.put(message);
    }

    private void sendSupportedEncodingsMessage(ProtocolSettings settings) {
        decoders.instantiateDecodersWhenNeeded(settings.encodings);
        SetEncodingsMessage encodingsMessage = new SetEncodingsMessage(settings.encodings);
        sendMessage(encodingsMessage);
        logger.fine("sent: " + encodingsMessage.toString());
    }

    /**
     * create pixel format by bpp
     */
    private PixelFormat createPixelFormat(ProtocolSettings settings) {
        int serverBigEndianFlag = serverPixelFormat.bigEndianFlag;
        switch (settings.getBitsPerPixel()) {
            case ProtocolSettings.BPP_32:
                return PixelFormat.create32bppPixelFormat(serverBigEndianFlag);
            case ProtocolSettings.BPP_16:
                return PixelFormat.create16bppPixelFormat(serverBigEndianFlag);
            case ProtocolSettings.BPP_8:
                return PixelFormat.create8bppBGRPixelFormat(serverBigEndianFlag);
            case ProtocolSettings.BPP_6:
                return PixelFormat.create6bppPixelFormat(serverBigEndianFlag);
            case ProtocolSettings.BPP_3:
                return PixelFormat.create3bppPixelFormat(serverBigEndianFlag);
            case ProtocolSettings.BPP_SERVER_SETTINGS:
                return serverPixelFormat;
            default:
                // unsupported bpp, use default
                return PixelFormat.create32bppPixelFormat(serverBigEndianFlag);
        }
    }

    @Override
    public void settingsChanged(SettingsChangedEvent e) {
        ProtocolSettings settings = (ProtocolSettings) e.getSource();
        if (settings.isChangedEncodings()) {
            sendSupportedEncodingsMessage(settings);
        }
        if (settings.changedBitsPerPixel() && receiverTask != null) {
            receiverTask.queueUpdatePixelFormat(createPixelFormat(settings));
        }
    }

    @Override
    public void sendRefreshMessage() {
        sendMessage(new FramebufferUpdateRequestMessage(0, 0, fbWidth, fbHeight, false));
        logger.fine("sent: full FB Refresh");
    }

    @Override
    public void cleanUpSession(String message) {
        cleanUpSession();
        rfbSessionListener.rfbSessionStopped(message);
    }

    public synchronized void cleanUpSession() {
        if (senderTask != null) {
            senderTask.stopTask();
        }
        if (receiverTask != null) {
            receiverTask.stopTask();
        }
        if (senderTask != null) {
            try {
                senderThread.join(1000);
            } catch (InterruptedException e) {
                // nop
            }
            senderTask = null;
        }
        if (receiverTask != null) {
            try {
                receiverThread.join(1000);
            } catch (InterruptedException e) {
                // nop
            }
            receiverTask = null;
        }
    }
}
